
#define GLM_FORCE_SWIZZLE
#include "SceneRenderer.hpp"

#include "Exception.hpp"
#include "TexData.hpp"
#include "LoadTexture.hpp"
#include <vector>
#include <glm/gtx/transform.hpp>
#include <iostream>
#include <chrono>
#include <fstream>

using namespace std;

const auto pi = glm::pi<float>();



const string SkydomeVertexSource = R"glsl(#version 450
layout(location=0) in vec3 attPosition;
layout(location=1) in vec3 attNormal;
layout(location=2) in vec2 attUv;

out vec2 varUv;
layout(location=0) uniform mat4 cameraTransform;
layout(location=1) uniform vec3 cameraPosition;

void main(void)
{
	gl_Position = cameraTransform*vec4(attPosition-cameraPosition, 1);
	varUv = attUv;
}
)glsl";


// --- Fragment Shader ---
const string SkydomeFragmentSource = R"glsl(#version 450

in vec2 varUv;
layout(binding=0) uniform sampler2D SkyMap;

void main(void)
{
	gl_FragColor = texture(SkyMap, varUv);
}
)glsl";


// our vertex format:
struct Vertex
{
	vec3 position;
	vec3 normal;
	vec2 uv;
};

void LoadObjIntoBuffer(std::string filename, VertexBuffer& outputBuffer)
{
	auto objContent = LoadObjFile(filename);

	if(objContent.size() != 1)
		THROW(Exception("file should have exactly one model..."));

	auto& objModel = *objContent.begin();

	vector<Vertex> vertexData;
	auto numVertices = objModel.positions.size();
	vertexData.reserve(numVertices);
	for(auto i=0u; i<numVertices; ++i)
	{
		vertexData.push_back({objModel.positions[i], objModel.normals[i], objModel.texCoords[i]});
	}
	outputBuffer.SetData(vertexData);
	outputBuffer.SetLocation(0, VERTEX_BUFFER_MEMBER(Vertex, position));
	outputBuffer.SetLocation(1, VERTEX_BUFFER_MEMBER(Vertex, normal));
	outputBuffer.SetLocation(2, VERTEX_BUFFER_MEMBER(Vertex, uv));
}


SceneRenderer::SceneRenderer():
InitFirst([this]()
{
	if(!glfwInit())
		THROW(Exception("GLFW could not be initialized"));
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
	m_Window = glfwCreateWindow(1024, 768, "loading...", NULL, NULL);
	if(!m_Window)
	{
		glfwTerminate();
		THROW(Exception("failed to open window"));
	}

	glfwMakeContextCurrent(m_Window);

	if(ogl_LoadFunctions() == ogl_LOAD_FAILED)
		THROW(Exception("Loading GL extensions failed..."));
}),
m_Distance(0.01),
m_CameraLookAt(0, 0, 1),
m_mode(1),
m_OriginalTexture(0),
m_LowFreqTexture(0),
m_HighFreqTexture(0),
m_SkydomeTexture(0),
m_TextureJustToggled(false)
{
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_CULL_FACE);
	glFrontFace(GL_CW);
	glCullFace(GL_FRONT);
	glClearColor(0.6, 0.8, 1, 1);

	auto config = TexData::LoadFile("config.cfg");

	m_CameraMoveSpeed = config["Camera"]["MoveSpeed"].as<float>();
	m_CameraRotateSpeed = config["Camera"]["RotateSpeed"].as<float>();
	m_CameraZoomSpeed = config["Camera"]["ZoomSpeed"].as<float>();
	m_CameraPerspective = config["Camera"]["Perspective"].as<float>();

	// initialize texture list
	{
		m_TextureCycle = {"Clay", "Cobblestone", "ForestGround",
			"Grass-5", "Grass-10", "Grass-15", "Grass-25", "Grass-50",
			"Grass-100", "Sand", "StoneWall", "Tiles", "WoodPlanks"};
		m_CurrentTextureInCycle = m_TextureCycle.begin();
	}


	// Load Terrain Model
	{
		LoadObjIntoBuffer(config["Terrain"].Value(), m_TerrainBuffer);
		m_TerrainShader.Load("Terrain.vs", "Terrain.fs");
		auto texture = config["Texture"]["Folder"].Value();
		LoadTerrainTexture(texture);
		m_CurrentTextureInCycle = std::find(m_TextureCycle.begin(), m_TextureCycle.end(), texture);
	}

	// load Skydome
	{
		LoadObjIntoBuffer("Skydome.obj", m_SkydomeBuffer);
		m_SkydomeShader.Create(SkydomeVertexSource, SkydomeFragmentSource);
		m_SkydomeTexture = LoadTexture("Skydome.jpg");
	}
}


void SceneRenderer::LoadTerrainTexture(std::string folder)
{
	// delete textures
	if(m_OriginalTexture)
		glDeleteTextures(1, &m_OriginalTexture);
	if(m_LowFreqTexture)
		glDeleteTextures(1, &m_LowFreqTexture);
	if(m_HighFreqTexture)
		glDeleteTextures(1, &m_HighFreqTexture);

	m_OriginalTexture = LoadTexture(folder+"/Original.png");
	m_LowFreqTexture = LoadTexture(folder+"/LowFreq.png");
	m_HighFreqTexture = LoadTexture(folder +"/HighFreq.png");
	m_Tiling = TexData::LoadFile(folder +"/tiling.cfg")["Tiling"].as<float>();

	glfwSetWindowTitle(m_Window, ("Terrain Demo  "+ folder).c_str());
}


void SceneRenderer::Run()
{
	using namespace std::chrono;
	high_resolution_clock clock;

	auto lastTime = clock.now();
	while(!glfwWindowShouldClose(m_Window))
	{
		Render();

		auto currentTime = clock.now();
		auto t = (duration_cast<milliseconds>(currentTime - lastTime).count()) / 1000.f;
		lastTime = currentTime;

		glfwPollEvents();
		ProcessInput(t);
	}

	glfwTerminate();
}


void SceneRenderer::Render()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glfwGetFramebufferSize(m_Window, &m_WindowResolution.x, &m_WindowResolution.y);
	glViewport(0, 0, m_WindowResolution.x, m_WindowResolution.y);

	m_SkydomeShader.Set();
	m_SkydomeShader.SetUniform(0, GetProjection()*GetCamera()*glm::scale(vec3(50))*glm::translate(vec3(0, 0, -2)));
	m_SkydomeShader.BindTexture(m_SkydomeTexture, 0);
	m_SkydomeBuffer.Draw();

	m_TerrainShader.Set();
	m_TerrainShader.SetUniform(0, GetProjection()*GetCamera());
	m_TerrainShader.SetUniform(10, m_mode);
	m_TerrainShader.SetUniform(11, m_Tiling);
	m_TerrainShader.BindTexture(m_OriginalTexture, 0);
	m_TerrainShader.BindTexture(m_LowFreqTexture, 1);
	m_TerrainShader.BindTexture(m_HighFreqTexture, 2);
	m_TerrainBuffer.Draw();

	glfwSwapBuffers(m_Window);
}

mat4 SceneRenderer::GetProjection()
{
	float aspectRatio;
	if(0 == m_WindowResolution.y)
		aspectRatio = 1.f;
	else
		aspectRatio = static_cast<float>(m_WindowResolution.x)/static_cast<float>(m_WindowResolution.y);
	return glm::perspective(pi*m_CameraPerspective, aspectRatio, 0.01f, 1000.f);
}

mat4 SceneRenderer::GetCamera()
{
	vec3 camPos = vec3(cos(m_ViewAngle.x) * cos(m_ViewAngle.y),
		sin(m_ViewAngle.x) * cos(m_ViewAngle.y),
		sin(m_ViewAngle.y)) * m_Distance;

	return glm::lookAt(camPos+m_CameraLookAt, m_CameraLookAt, vec3(0, 0, 1));
}


std::string SceneRenderer::PrintControls()
{
	return R"text(
 _____             _             _         
/  __ \           | |           | |        
| /  \/ ___  _ __ | |_ _ __ ___ | |___     
| |    / _ \| '_ \| __| '__/ _ \| / __|    
| \__/\ (_) | | | | |_| | | (_) | \__ \    
 \____/\___/|_| |_|\__|_|  \___/|_|___/                            
 ______ ______ ______ ______ ______ ______ 

    Input               |  Action
    --------------------|----------------
    Left mouse button   |  Rotate camera
    Right mouse button  |  Zoom camera
    `W`, `A`, `S`, `D`  |  Move camera
    `R`, `F`            |  Move camera up/down
    `T`, `G`            |  Next/previous texture
    `1`                 |  Tiled texture mode
    `2`                 |  Detail mapping mode
    Alt + Numpad [0..9] |  Save camera 0..9
    Numpad [0..9]       |  Load camera 0..9
)text";
}


void SceneRenderer::ProcessInput(float t)
{
	double xpos, ypos;
	glfwGetCursorPos(m_Window, &xpos, &ypos);
	auto newPos = vec2(xpos, ypos);

	
	auto diff = (newPos-m_MousePos)/100.f;
	if(glfwGetMouseButton(m_Window, GLFW_MOUSE_BUTTON_1))
	{
		m_ViewAngle += vec2(-diff.x, diff.y) * m_CameraRotateSpeed;
	}
	if(glfwGetMouseButton(m_Window, GLFW_MOUSE_BUTTON_2))
	{
		m_Distance *= exp(diff.y*m_CameraZoomSpeed);
		m_Distance = std::max(m_Distance, 0.01f);
	}

	// move camera
	auto cameraToWorld = glm::inverse(GetCamera());
	if(glfwGetKey(m_Window, GLFW_KEY_W))
	{
		m_CameraLookAt += vec3(cameraToWorld*vec4(0, 0, -1, 0)*t*m_CameraMoveSpeed);
	}
	if(glfwGetKey(m_Window, GLFW_KEY_S))
	{
		m_CameraLookAt += vec3(cameraToWorld*vec4(0, 0, 1, 0)*t*m_CameraMoveSpeed);
	}
	if(glfwGetKey(m_Window, GLFW_KEY_A))
	{
		m_CameraLookAt += vec3(cameraToWorld*vec4(-1, 0, 0, 0)*t*m_CameraMoveSpeed);
	}
	if(glfwGetKey(m_Window, GLFW_KEY_D))
	{
		m_CameraLookAt += vec3(cameraToWorld*vec4(1, 0, 0, 0)*t*m_CameraMoveSpeed);
	}
	if(glfwGetKey(m_Window, GLFW_KEY_R))
	{
		m_CameraLookAt += vec3(cameraToWorld*vec4(0, 1, 0, 0)*t*m_CameraMoveSpeed);
	}
	if(glfwGetKey(m_Window, GLFW_KEY_F))
	{
		m_CameraLookAt += vec3(cameraToWorld*vec4(0, -1, 0, 0)*t*m_CameraMoveSpeed);
	}

	// switch render modes
	if(glfwGetKey(m_Window, GLFW_KEY_1))
	{
		m_mode = 0;
	}
	if(glfwGetKey(m_Window, GLFW_KEY_2))
	{
		m_mode = 1;
	}

	// toggle textures
	if(glfwGetKey(m_Window, GLFW_KEY_T))
	{
		if(!m_TextureJustToggled)
		{
			m_CurrentTextureInCycle++;
			if(m_CurrentTextureInCycle == m_TextureCycle.end())
				m_CurrentTextureInCycle = m_TextureCycle.begin();
			LoadTerrainTexture(*m_CurrentTextureInCycle);
		}
		m_TextureJustToggled = true;
	}
	else if(glfwGetKey(m_Window, GLFW_KEY_G))
	{
		if(!m_TextureJustToggled)
		{
			if(m_CurrentTextureInCycle == m_TextureCycle.begin())
				m_CurrentTextureInCycle = m_TextureCycle.end();
			m_CurrentTextureInCycle--;
			LoadTerrainTexture(*m_CurrentTextureInCycle);
		}
		m_TextureJustToggled = true;
	}
	else
	{
		m_TextureJustToggled = false;
	}


	auto SaveCam = [this](std::string filename){
		ofstream f(filename);
		f << m_Distance << "\n"
		<< m_ViewAngle.x << "\n"
		<< m_ViewAngle.y << "\n"
		<< m_CameraLookAt.x << "\n"
		<< m_CameraLookAt.y << "\n"
		<< m_CameraLookAt.z << "\n";
	};

	auto LoadCam = [this](std::string filename){
		ifstream f(filename);
		f >> m_Distance >> m_ViewAngle.x >> m_ViewAngle.y
			>> m_CameraLookAt.x >> m_CameraLookAt.y >> m_CameraLookAt.z;
	};
	// loading & saving of camera positions:
	for(int i=0; i<10; ++i)
	{
		if(glfwGetKey(m_Window, GLFW_KEY_KP_0+i))
		{
			auto fn = "SavedCameras/Cam"+std::to_string(i)+".txt";
			if(glfwGetKey(m_Window, GLFW_KEY_LEFT_ALT))
				SaveCam(fn);
			else
				LoadCam(fn);
		}
	}


	m_MousePos = newPos;
}
